<?php
/**
 * GeryEngine
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this release in the file LICENSE
 *
 * @copyright  Copyright (c) 2007 Arjen Brouwer
 * @license    New BSD License
 * @author     Arjen Brouwer
 */
 
%%
%namespace Mono.CSharp
%public
%class Lexer
%implements yyParser.yyInput
%type int
%ignore_token null

%eofval{
	return false;
%eofval}

%full
%line
%char
%state IN_REFERENCE IN_PARAMETER IN_MULTILINE_COMMENT IN_COMMENT IN_DIRECTIVE IN_DOUBLE_QUOTES IN_INTERPOLATE IN_START_DIRECTIVE
 
%{
    private $compileEngine;
    private $stateStack;
    private $parenCount;
    private $refBraceCount = 0;
    private $commentStart = 0;
    
	function raiseError($msg, $code, $fatal) {
		$msg .= " on line " . ($this->yyline + 1) . ' in state ' . $this->getStateName($this->getState());
		
        throw new GeryParseErrorException($msg);
    }
    
    public function getStateName($state) {
    	$class = new ReflectionClass('Lexer');
    	$stateName = 'YY_INITIAL';
        
    	foreach ($class->getConstants() as $name => $value) {
        	if (substr($name, 0, 3) == 'IN_' && $value == $state) {
        		$stateName = $name;
        	}
        }
        
        return $stateName;
    }
    
    public function getState() {
    	return $this->yy_lexical_state;
    }
    
    public function getStates() {
    	if (! isset($this->stateStack))
    		return array();
    		
    	return $this->stateStack;
    }
    
    public function reset() {
    	$this->yy_buffer_index = 0;
        $this->yy_buffer_start = 0;
        $this->yy_buffer_end = 0;
        $this->yychar = 0;
        $this->yyline = 0;
        $this->yy_at_bol = true;
        $this->yy_lexical_state = self::YYINITIAL;
	}
	
	public function setInput($input) {
		$this->yy_buffer = $input;
		 $this->yy_buffer_read = strlen($input);
	}
    
	public function setCompileEngine($engine) {
    	$this->compileEngine = $engine;
    }
    
    private function pushState() {
    	$this->stateStack[] = $this->yy_lexical_state;
    }
    
    public function popState() {
    	if (! count($this->stateStack)) {
    		$this->yy_lexical_state = self::YYINITIAL;
    	} else {
    		$this->yy_lexical_state = array_pop($this->stateStack);
    	}
    	
    	return $this->yy_lexical_state;
    }
    
    public function fallbackToState($state) {
    	
    	while (($poppedState = $this->popState()) !== null) {
    		if ($poppedState == $state)
    			return;
    	}
    }
    
    private function newToken($type, $value = null) {
       	
    	$token = new Token();
    	$token->type = $type;
    	$token->value = $value;
    	$token->offset = $this->yy_buffer_index - $this->yylength();
    	
    	// add one to the linenumber because it starts counting at zero
    	$token->line = $this->yyline + 1;
    	
    	return $token;
    }
    
    public function getCharacter() {
    	return $this->yychar;
    }
%}

DIGIT			= [0-9]
WHITESPACE		= [ \t]
NEW_LINE		= [\n\r]

SMURK 			= [^#$\\]+
IDENTIFIER 		= [a-zA-Z][a-zA-Z_0-9]*
NUMBER			= (\-|\+)?{DIGIT}+(\.{DIGIT}+)?

%%

// start multiline comment
<YYINITIAL, IN_DIRECTIVE, IN_PARAMETER> "#*" {
	if ($this->yy_lexical_state == self::YYINITIAL) 
	{
		$this->commentStart = $this->yy_buffer_index - $this->yylength();
		$this->yybegin(self::IN_MULTILINE_COMMENT);
		return null;
	} else {
		return $this->newToken(TK_SMURK, $this->yytext());
	}
	
}

// start single line comment
<YYINITIAL> "##" {
	$this->yybegin(self::IN_COMMENT);
	$this->commentStart = $this->yy_buffer_index - $this->yylength();
	return null;
}

<YYINITIAL> (\\)+"$" {
	$escapes = substr($this->yytext(), 0, -1);
	$this->yy_buffer_index--;
	
	return $this->newToken(TK_ESCAPES, $escapes);
}

<YYINITIAL> (\\)(\\\\)*"#" {
	
	$pos = strrpos($this->yytext(), '\\');
	
	$escapes = str_repeat('\\', $pos / 2);
	
	$ret = $escapes . substr($this->yytext(), $pos + 1);
	
	return $this->newToken(TK_SMURK, $ret);
}

<YYINITIAL> (\\)+ {
	return $this->newToken(TK_SMURK, $this->yytext());
}

<YYINITIAL> "#end"|"#{end}" {
	return $this->newToken(TK_END);
}

<YYINITIAL> "#elseif"|"#{elseif}" {
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	return $this->newToken(TK_ELSEIF);
}

<YYINITIAL> "#else"|"#{else}" {
	return $this->newToken(TK_ELSE);
}

<YYINITIAL> "#macro"|"#{macro}" {
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	return $this->newToken(TK_MACRO);
}

<YYINITIAL> "#if"|"#{if}" {
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	return $this->newToken(TK_IF);
}

<YYINITIAL> "#foreach"|"#{foreach}" {
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	return $this->newToken(TK_FOREACH);
}

<YYINITIAL> "#local"|"#{local}" {
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	return $this->newToken(TK_LOCAL);
}

<YYINITIAL> "#set"|"#{set}" {
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	return $this->newToken(TK_SET);
}

<YYINITIAL> "#stop"|"#{stop}" {
	return $this->newToken(TK_STOP);
}

<YYINITIAL> "#parse"|"#{parse}" {
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	return $this->newToken(TK_PARSE);
}

<YYINITIAL> "#include"|"#{include}" {
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	return $this->newToken(TK_INCLUDE);
}


<YYINITIAL> ("#"({IDENTIFIER})) {

	$macro = substr($this->yytext(), 1);
	
	// this is tricky
	$i = $this->yy_buffer_end;
	
	do {
		$c = substr($this->yy_buffer, $i, 1);
	} while (in_array($c, array(" ", "\t")));
	
	if ($c == "(") {
		$this->pushState();
		$this->yybegin(self::IN_START_DIRECTIVE);
		
		return $this->newToken(TK_DIRECTIVE, $macro);		
	} else {
		return $this->newToken(TK_SMURK, $this->yytext());
	}
}

<YYINITIAL> "#{" {
	// go into interpolation state
	$this->pushState();
	$this->yybegin(self::IN_INTERPOLATE);
	
	$this->yy_buffer_index--;
	return $this->newToken(TK_INTERPOLATE);
}

<IN_INTERPOLATE> "{" {
	$this->pushState();
	$this->yybegin(self::IN_PARAMETER);
	return $this->newToken(TK_LBRACE);
}

<IN_INTERPOLATE> "}" {
	// bail out of the interpolation state
	$this->popState();
	
	return $this->newToken(TK_RBRACE);
}

<IN_DIRECTIVE, IN_PARAMETER> ("#"{IDENTIFIER}) {

	$macro = substr($this->yytext(), 1);
	
	// this is a registered macro goto macro state
	$this->pushState();
	$this->yybegin(self::IN_START_DIRECTIVE);
	
	return $this->newToken(TK_DIRECTIVE, $macro);
}

<YYINITIAL> "#" {
	return $this->newToken(TK_SMURK, $this->yytext());
}

<YYINITIAL> {SMURK} {
	$smurk = $this->yytext();
	return $this->newToken(TK_SMURK, $this->yytext());
}

<YYINITIAL, IN_PARAMETER, IN_DIRECTIVE> "$"("!"{IDENTIFIER}|"!{"{IDENTIFIER}|"{"{IDENTIFIER}|{IDENTIFIER}) {
	
	$this->pushState();
	$this->yybegin(self::IN_REFERENCE);
	
	$token = $this->newToken(TK_DOLLAR);
	$this->yy_buffer_index -= ($this->yylength() - 1);
	
	return $token;
}

<YYINITIAL> "$" {
	$smurk = $this->yytext();
	return $this->newToken(TK_SMURK, $this->yytext());
}

<IN_DIRECTIVE> "or"|"||" {
	return $this->newToken(TK_OR);
}

<IN_DIRECTIVE> "and"|"&&" {
	return $this->newToken(TK_AND);
}

<IN_START_DIRECTIVE> ({WHITESPACE})+ {
	// skip whitespace
	return null;
}

<IN_START_DIRECTIVE> "(" {
	$this->yybegin(self::IN_DIRECTIVE);
	return $this->newToken(TK_LPAREN);
}

<IN_START_DIRECTIVE> [^\( \t]+ {
	return $this->newToken(TK_SMURK, $this->yytext());
}

<IN_DIRECTIVE> "(" {
	$this->pushState();
	$this->yybegin(self::IN_DIRECTIVE);
	
	return $this->newToken(TK_LPAREN);
}

<IN_DIRECTIVE> ")" {
	$this->popState();
	return $this->newToken(TK_RPAREN);
}

<IN_DIRECTIVE> "in" {
	return $this->newToken(TK_IN);
}

<IN_DIRECTIVE> {IDENTIFIER} {
	return $this->newToken(TK_ID, $this->yytext());
}


<IN_DIRECTIVE, IN_PARAMETER, IN_REFERENCE> "[" {
	$this->pushState();
	$this->yybegin(self::IN_PARAMETER);
	
	return $this->newToken(TK_LBRACKET);
}

<IN_DIRECTIVE, IN_PARAMETER> "!" {
	return $this->newToken(TK_NOT);
}

<IN_DIRECTIVE, IN_PARAMETER> "+" {
	return $this->newToken(TK_PLUS);
}

<IN_DIRECTIVE, IN_PARAMETER> "-" {
	return $this->newToken(TK_MIN);
}

<IN_DIRECTIVE, IN_PARAMETER> "*" {
	return $this->newToken(TK_STAR);
}


<IN_DIRECTIVE, IN_PARAMETER> "%" {
	return $this->newToken(TK_PERCENT);
}

<IN_DIRECTIVE, IN_PARAMETER> "/" {
	return $this->newToken(TK_SLASH);
}

<IN_DIRECTIVE, IN_PARAMETER> "==" {
	return $this->newToken(TK_EQUAL);
}

<IN_DIRECTIVE, IN_PARAMETER> "!=" {
	return $this->newToken(TK_NOT_EQUAL);
}

<IN_DIRECTIVE, IN_PARAMETER> ">" {
	return $this->newToken(TK_GT);
}

<IN_DIRECTIVE, IN_PARAMETER> "<" {
	return $this->newToken(TK_LT);
}

<IN_DIRECTIVE, IN_PARAMETER> ">=" {
	return $this->newToken(TK_GE);
}

<IN_DIRECTIVE, IN_PARAMETER> "<=" {
	return $this->newToken(TK_LE);
}


<IN_DIRECTIVE> "=" {
	return $this->newToken(TK_IS);
}

<IN_REFERENCE> "{" {
	$this->refBraceCount++;
	return $this->newToken(TK_LBRACE);
}

<IN_REFERENCE, IN_PARAMETER> "}" {
	if ($this->refBraceCount > 0) {
		$this->refBraceCount--;
		$this->popState();
		
		return $this->newToken(TK_RBRACE);
	} else {
		// a right brace is encountered, this state is called from the 
		// IN_INTERPOLATION state
		
		// pop the current state of the stack
		$this->fallbackToState(self::IN_INTERPOLATE);
		
		$this->yy_buffer_index--;
		
		// return a right brace
		return null;	
	}
}

<IN_REFERENCE, IN_PARAMETER> {IDENTIFIER} {
	return $this->newToken(TK_ID, $this->yytext());
}

<IN_REFERENCE> "!" {
	// the SILENT token
	
	// get the preceding character
	$precedingCharacter = substr($this->yy_buffer, $this->yy_buffer_index - 2, 1);
	
	// if a dollar sign precedes this silent token its ok
	if ($precedingCharacter == '$') {
		// return a silencing token
		return $this->newToken(TK_SILENT);
		
	} else {
		$this->yy_buffer_index--;
		$this->popState();
		
		return null;
	}
}

<IN_REFERENCE, IN_PARAMETER, IN_DIRECTIVE> "." {
	return $this->newToken(TK_DOT);
}

<IN_REFERENCE> "(" {
	// push current state on the stack
	$this->pushState();
	$this->yybegin(self::IN_PARAMETER);
	
	return $this->newToken(TK_LPAREN);
}

<IN_REFERENCE> ({WHITESPACE})+ {
	$this->popState();
	
	if ($this->yy_lexical_state == self::YYINITIAL) {
		return $this->newToken(TK_SMURK, $this->yytext());
	} else {
		// skip whitespace if we are not in smurk
		return null;
	}
}

<IN_REFERENCE> [^\.a-z] {
	// get the previous state from stack
	$this->popState();
	
	$this->yy_buffer_index--;
	return null;
}

<IN_PARAMETER, IN_DIRECTIVE> {WHITESPACE} {
	// ignore
	return null;
}

<IN_PARAMETER, IN_DIRECTIVE, IN_REFERENCE> ".." {
	return $this->newToken(TK_DOTDOT);
}

<IN_PARAMETER> ")" {
	// get state from the stack
	$this->popState();
	
	return $this->newToken(TK_RPAREN);
}

<IN_PARAMETER> "(" {
	$this->pushState();
	$this->yybegin(self::IN_PARAMETER);
	
	return $this->newToken(TK_LPAREN);
}

<IN_PARAMETER> "]" {
	// get state from the stack
	$this->popState();

	return $this->newToken(TK_RBRACKET);
}

<IN_PARAMETER, IN_DIRECTIVE> ":" {
	return $this->newToken(TK_COLON);
}

<IN_PARAMETER, IN_DIRECTIVE> "," {
	return $this->newToken(TK_COMMA);
}

<IN_PARAMETER, IN_DIRECTIVE> {NUMBER} {
	return $this->newToken(TK_NUMBER, $this->yytext());
}

//
// Match a quoted string
//
<IN_PARAMETER, IN_DIRECTIVE> (\"(\\.|[^\"\\])*\"|\'(\\.|[^\'\\])*\')  {
	
	$string = substr($this->yytext(), 1, -1);
	$type = $this->yy_buffer[$this->yy_buffer_start];
	
	if ($type == "\"") {
		$string = str_replace('\"', '"', $string);
		$string = str_replace("'", "\\'", $string);
	} else {
		
	}
	
	return $this->newToken(TK_STRING, $string);
}

<IN_PARAMETER, IN_DIRECTIVE, IN_REFERENCE> ([\n\r]) {
	// new lines are ignored in directives
	return null;
}

<IN_COMMENT> [^\n\r]+ {
	// ignore comment
	return null;
}

<IN_COMMENT> {NEW_LINE} {

	$start = $this->commentStart;
	$length = $this->yy_buffer_index - $start;
	
	$this->yybegin(self::YYINITIAL);
	return $this->newToken(TK_COMMENT, $this->compileEngine->getInputSubString($start, $length));
}


<IN_MULTILINE_COMMENT> "*#" {
	$start = $this->commentStart;
	$length = $this->yy_buffer_index - $start;

	// goto previous state
	$this->popState();
	
	return $this->newToken(TK_MULTILINE_COMMENT, $this->compileEngine->getInputSubString($start, $length));
}

<IN_MULTILINE_COMMENT> ([^*]|"*"[^#])+ {
	// ignore comment
	return null;
}